#!/usr/bin/env ruby

# (c) Schubert

module Music
  require 'set'

  INTERVALS = ['1', 'b2', '2', 'b3', '3', '4', 'b5', '5', 'b6', '6', 'b7', '7']
  SWARAS    = ['Sa','re', 'Re', 'ga',  'Ga',  'ma', 'Ma','Pa','da', 'Da', 'ni',  'Ni']
  NOTES     = ['C', 'C#', 'D',  'Eb',  'E',   'F',  'F#','G', 'G#', 'A',  'Bb',  'B']
  TUNING = {
    :bass => ['E', 'A', 'D', 'G'],
    :standard => ['E', 'A', 'D', 'G', 'B', 'E'], 
    :drop_d => ['D', 'A', 'D', 'G', 'B', 'E'], 
    :drop_c => ['C','G','C','F','A','D']
  }
  
  CHORD = { # notes in chords and the correct mode for that chord
    # SIMPLE CHORDS (3 NOTE CHORDS)
    :major => ['1', '3', '5'],
    :minor => ['1', 'b3', '5'], # *
    :diminished => ['1', 'b3', 'b5'], # *
    :augmented => ['1', '3', 'b6'], # *

    # JAZZ CHORDS 
    :major_seven => ['1', '3', '5', '7'],
    :minor_seven => ['1', 'b3', '5', 'b7'],
    :diminished_seven => ['1', 'b3', 'b5', '6'], 

    :major_six => ['1', '3', '5', '6'],

    :minor_major_seven => ['1', 'b3', '5', '7'], 
    :dominant_seven => ['1', '3', '5', 'b7'], 

    # ADVANCED JAZZ CHORDS

    # aka the half diminished chord
    :minor_seven_flat_five => ['1', 'b3', 'b5', 'b7'], 

    # same as 7(#4) 7(#11) 7(b5)
    :dominant_seven_flat_five => ['1', '3', 'b5', 'b7'], 

    # same as maj7(#4) maj7(#11) maj7(b5)
    :major_seven_flat_five => ['1', '3', 'b5', '7'], 

    # SUS CHORDS
    :suspended_two => ['1', '5', '2'], #opeth
    :suspended_four => ['1', '5', '4'],

    # JAZZ SUS CHORDS
    :dominant_seven_suspended => ['1', '5', '7'], # major seven without the 3rd
    :minor_seven_suspended => ['1', '5', 'b7'], # minor seven without the 3rd

    # ADD 9ths
    :major_add_nine => ['1', '3', '5', '2'], 
    :minor_add_nine => ['1', 'b3', '5', '2'],

    # JAZZ 9ths
    :major_seven_add_nine => ['1', '3', '5', '7', '2'], 
    :minor_seven_add_nine => ['1', 'b3', '5', 'b7', '2'],

    # UNKNOWN DO THEY EXIST/ ARE THEY CORRECT
    # :minor_seven_sharp_five => ['1', 'b3', 'b6', 'b7'],  # PHRYGIAN/LOCRIAN modes
    # :minor_add_nine_flat_five => ['1', 'b3', 'b5', '2'], # opeth

    # DIADS (DISTORTION FRIENDLY)

    :five => ['1', '5'],
    :flat_five => ['1', 'b5'],
    :augmented_five => ['1', 'b6'],

    # OCTAVE DOUBLER
    :octave_double => ['1'] 
    # :major_seven_sharp_4 => ['1', '3', '#4', '5', '7', '2'] ## opeth????
  }
  
  MODE = {
    :major =>  
    {
      :ionian => 0,
      :dorian => 2,
      :phrygian => 4,
      :lydian => 5,
      :mixolydian => 7,
      :aeolian => 9,
      :locrian => 11,
    },
    :harmonic_minor => 
    {
      :phrygian => 7
    }
  }
  
  SCALE = { # notes in scale
    :chromatic => ['1', 'b2', '2', 'b3', '3', '4', 'b5', '5', 'b6', '6', 'b7', '7'],
    :major => ['1', '2', '3', '4', '5', '6', '7'],
    :minor => ['1', '2', 'b3', '4', '5', 'b6', 'b7'],
    :harmonic_minor => ['1', '2', 'b3', '4', '5', 'b6', '7'],
    :melodic_minor => ['1', '2', 'b3', '4', '5', '6', '7'],
    :major_pentatonic => ['1', '2', '3', '5', '6'],
    :minor_pentatonic => ['1', 'b3', '4', '5', 'b7'],
    :blues => ['1', 'b3', '4', 'b5', '5', 'b7']
  }

  RAGA = {
    :desh => { # evening-night raga (6pm - 9pm )
      :aroha => ['1', '2', '4', '5', '7'],
      :avaroha => ['1', 'b7', '6', '5', '4', '3', '2' ,'3', '7', '1'], 
      :pakad => []
    },
    :megh => { # rainy season raga
      :aroha => ['1', '2', '4', '5', 'b7', '7'] 
    }
  }
  
  def self.harmonize(notes, scale, harmony)
    notes.map{|x| scale.notes[(scale.notes.index(x) + (harmony-1)) % scale.notes.length]}
  end

  def self.transpose(notes, relative)
    notes.collect{|i| get_note(NOTES.index(i) + relative)}
  end
  
  def self.get_notes(root, intervals)
    notes = []
    for interval in intervals
      notes << get_note(root + interval)
    end
    notes
  end
  
  def self.get_note(pos)
    NOTES[pos % 12]
  end
  
  def self.bass(notes, options = {})
    guitar(notes, {:tuning => TUNING[:bass]})
  end

  def self.guitar(plot, options = {})
    options[:frets] ||= 15
    options[:tuning] ||= TUNING[:standard] 

    root, notes, third, fifth, seventh = []
    if plot.is_a?(Mode) 
      notes, root, third, fifth, seventh = [plot.notes, plot.root, plot.third, plot.fifth, plot.seventh]
    elsif plot.is_a?(Chord) or plot.is_a?(Scale)
      notes, root= [plot.notes, plot.root]
    elsif plot.is_a?(Array)
      root, notes = [plot[0], plot]
    else
      puts "Unable to plot #{plot.class}"
      return
    end
    
    char = '-x' if options[:dont_show_notes]
    print "\n"

    options[:tuning].reverse.each do |string|
      0.upto(options[:frets]) do |fret|
        note = get_note(NOTES.index(string) + fret)
        print (fret == 0 ? '-' : '-' * (options[:frets] - fret))
        if i = notes.index(note)
          if note == root
            print "--\e[3;31;1m#{char || note.ljust(2,'-')}\e[0m-|"
          elsif third and note == third
            print "--\e[3;36;1m#{char || note.ljust(2,'-')}\e[0m-|"
          elsif fifth and note == fifth
            print "--\e[3;34;1m#{char || note.ljust(2,'-')}\e[0m-|"
          else
            print "--#{char || note.ljust(2,'-')}-|"
          end
        else
          print '-----|'
        end

        print '|' if fret == 0
      end # end frets
      print "\n"
    end # end strings

    print "\n"
    0.upto(options[:frets]) do |fret|
      print (fret == 0 ? ' ' : ' ' * (options[:frets] - fret))
      # print ' ' * (options[:frets] - fret)
      print [3, 5, 7, 9, 12, 15, 17, 19, 21].member?(fret) ? "  #{fret}   " : "      "
      print ' ' if fret == 0
    end
    print "\n"
    notes
    print "\n"
    puts notes.inspect
  end

  def self.chord_in_mode_scale?(chord, mode)
    c = chord.notes.to_set
    m = mode.notes.to_set
    c.subset?(m)
  end

# REWRITE USING CHORD CONSTRUCTION METHOD
#   def self.chords_in_key(key, chord_length = nil)
#     chords = []
#     m = Mode.new(key, :ionian)
#     CHORD.keys.each do |chord_type|
#       NOTES.each do |n|
#         c = Chord.new(n, chord_type)
#         if (chord_in_mode_scale?(c, m) and (c.notes.length == chord_length or chord_length.nil?))
#           chords << "#{n} #{chord_type}" 
#         end
#       end
#     end
#     chords
#   end

  # modulate from 1 scale to another scale (optional via chord)
  def self.modulater(scale1, scale2, via = nil)
    x = scale1.notes.to_set.intersection(scale2.notes.to_set)
    notes = via ? x.intersection(via.notes.to_set) : x
    possible_chords(notes)
  end 

  def self.possible_chords(notes, n = [])
    notes.each do |note|
      CHORD.select{|a,b| n.member?(b.length) or n.empty?}.each do |type, x|
        root = NOTES.index(note)
        intervals = x.collect{|i| INTERVALS.index(i)}
        puts note + ' ' + type.to_s if get_notes(root, intervals).to_set.subset?(notes.to_set)
      end	     
    end
  end 

  def self.possible_modes_scales(chords)
    # todo
  end

  class Scale
    attr_accessor :root, :type
    def initialize(root, type = :chromatic)
      @root = root
      @type = type
    end
    
    def notes
      root = NOTES.index(@root)
      notes = []
      for interval in SCALE[type].collect{|i| INTERVALS.index(i)}
        notes << Music.get_note(root + interval)
      end
      notes
    end
  end		

  class Mode
    attr_accessor :type, :root, :third, :fifth, :seventh

    def initialize(root, type = :ionian, scale = :major)
      @root = root
      @type = type
      @scale = scale
      @third = notes[(notes.index(root) + 2) % 7]
      @fifth = notes[(notes.index(root) + 4) % 7]
      @seventh = notes[(notes.index(root) + 6) % 7]
      # TODO
      # @ninth
      # @eleventh
      # @thirteenth
    end

    def notes
      fake_root = NOTES.index(@root) - MODE[@scale][@type]
      notes = []
      for interval in SCALE[@scale].collect{|i| INTERVALS.index(i)}
        notes << Music.get_note(fake_root + interval)
      end
      notes
    end
  end

  class Chord
    attr_accessor :root, :type

    def initialize(root, type = :major)
      @root = root
      @type = type
    end
    
    def to_s
      "#{root} #{type}"
    end
    
    def notes
      if @type.is_a?(Symbol)
        case @root[@root.length-1..@root.length]
        when '5'
          @type = :five
          @root = @root[0..@root.length-2]
        when 'm'
          type = :minor
          @root = @root[0..@root.length-2]
        end
        intervals = CHORD[@type].collect{|i| INTERVALS.index(i)}
      elsif @type.is_a?(Array)
        intervals = @type.collect{|x| (x.is_a?(String) ? INTERVALS.index(x) : x)}
      end
      root = NOTES.index(@root)
      Music.get_notes(root, intervals)
    end

  end

  class Progression
    attr_accessor :chords
    
    def initialize(chords = [])
      @chords = chords
    end
    
    def possible_key
      notes = []
      @chords.each do |chord|
        notes << chord.notes
      end
      notes.flatten!.uniq!
      notes.each do |note|
        SCALE.select{|x,y| x != :chromatic}.each do |type, x|
        root = NOTES.index(note)
        intervals = x.collect{|i| INTERVALS.index(i)}
        puts note + ' ' + type.to_s if notes.to_set.subset?(Music.get_notes(root, intervals).to_set)
      end	     
    end

    end
    
  end

end
